// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.IO;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Threading;
using Microsoft.TeamFoundation.VersionControl.Common;
using Microsoft.TeamFoundation.Migration.Toolkit;
using Microsoft.TeamFoundation.Migration.Toolkit.VC;
using System.Globalization;
using System.Data;
using System.Data.SqlClient;

namespace Microsoft.TeamFoundation.Migration.Toolkit
{
    /// <summary>
    /// Provides version control path and mapping methods that are common throughout the migration toolkit.
    /// </summary>
    public static class TfsUtil
    {
        public static bool IsOurTfsChange(Changeset changeset, string modifier)
        {
            if (changeset == null)
            {
                return false;
            }
            bool result = 
                string.Compare(changeset.Committer, changeset.VersionControlServer.AuthenticatedUser, 
                    true, CultureInfo.InvariantCulture) == 0 &&

                changeset.Comment != null &&
                changeset.Comment.Contains(modifier); // Migrated by ...
            
            return result;
        }

        /// <summary>
        /// Performs an Undo on any pending changes and
        /// removes all items from the workspace.
        /// </summary>
        /// <param name="activeWorkspace">The workspace to clean</param>
        public static void CleanWorkspace(Workspace activeWorkspace)
        {
            if (activeWorkspace == null)
            {
                throw new ArgumentNullException("activeWorkspace");
            }

            CleanWorkspaceFiles(activeWorkspace);

            PendingChange[] changes = activeWorkspace.GetPendingChanges();
            if (changes != null && changes.Length > 0)
            {
                activeWorkspace.Undo(changes);
            }

            TraceManager.WriteLine(TraceManager.Engine, "Calling get;C1 to clear workspace");
            activeWorkspace.Get(new ChangesetVersionSpec(1), GetOptions.Overwrite);
        }

        /// <summary>
        /// Given a change Id, return the linked change Id on the other system
        /// </summary>
        /// <param name="sessionId">versioncontrol session id</param>
        /// <param name="changeId">Source change Id if the sourceSystem is Other. TFS changeset id if the sourceSystem is Tfs.</param>
        /// <param name="sourceSystem"></param>
        /// <returns></returns>
        public static string GetChangeIdFromConversionHistory(string sessionId, string changeId, SystemType sourceSystem)
        {
           bool fromSource = (sourceSystem == SystemType.Other);
            int tfsChangesetId = 0;
            string sourceChangeId = string.Empty;
            if (fromSource)
            {
                sourceChangeId = changeId;
            }
            else
            {
                tfsChangesetId = int.Parse(changeId, CultureInfo.InvariantCulture);
            }

            using (SqlConnection conn = DataAccessManager.Current.GetSqlConnection())
            {
                conn.Open();
                using (SqlCommand cmd = new SqlCommand())
                {
                    cmd.Connection = conn;
                    cmd.CommandType = CommandType.StoredProcedure;
                    cmd.CommandText = "prc_QueryConversionHistory";

                    cmd.Parameters.Add("@SessionId", SqlDbType.NVarChar).Value = sessionId;
                    cmd.Parameters.Add("@SourceChangeId", SqlDbType.NVarChar).Value = sourceChangeId;
                    cmd.Parameters.Add("@TfsChangesetId", SqlDbType.Int).Value = tfsChangesetId;
                    cmd.Parameters.Add("@FromSource", SqlDbType.Bit).Value = fromSource;

                    using (SqlDataReader reader = cmd.ExecuteReader())
                    {
                        if (reader.Read())
                        {
                            if (fromSource)
                            {
                                return Convert.ToString((int)reader["TfsChangesetId"], CultureInfo.InvariantCulture);
                            }
                            else
                            {
                                return (string)reader["SourceChangeId"];
                            }
                        }
                        return null;
                    }
                }
            }
        }

        public static void CleanWorkspaceFiles(Workspace activeWorkspace)
        {
            if (activeWorkspace == null)
            {
                throw new ArgumentNullException("activeWorkspace");
            }

            // Flush to changeset 1 to cleanup any files
            TraceManager.WriteLine(TraceManager.Engine, "Clearing TFS workspace files");

            foreach (WorkingFolder wf in activeWorkspace.Folders)
            {
                if (!wf.IsCloaked)
                {
                    deleteFiles(wf.LocalItem);
                }
            }
        }

        // there was a high incidence of IO exceptions from files being locked when clearing the
        // directory contents.  This retry logic should help resolve any timing issues around
        // virus scanning or someone accidently locking a directory by being in it in a cmd window
        private static void deleteFiles(string directory)
        {
            const int maxAttempts = 20;
            int currentAttempt = 0;

            if (!Directory.Exists(directory))
            {
                return;
            }

            while (true)
            {
                try
                {
                    deleteFiles_inner(directory);
                    return;
                }
                catch (IOException ioe)
                {
                    currentAttempt++;

                    TraceManager.WriteLine(TraceManager.Engine, "Caught an IO exception cleaning the directory tree:");
                    TraceManager.WriteLine(TraceManager.Engine, ioe.Message);
                    TraceManager.WriteLine(TraceManager.Engine, "That was attempt {0} of {1}", currentAttempt, maxAttempts);

                    if (currentAttempt < maxAttempts)
                    {
                        TraceManager.WriteLine(TraceManager.Engine, "Sleeping for 30 seconds to let the IO issue resolve itself");
                        Thread.Sleep(30 * 1000);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
        }

        private static void deleteFiles_inner(string directory)
        {
            foreach (string file in Directory.GetFiles(directory))
            {
                TfsUtil.DeleteFile(file);
            }

            foreach (string subDirectory in Directory.GetDirectories(directory))
            {
                deleteFiles_inner(subDirectory);
                Directory.Delete(subDirectory);
            }
        }

        /// <summary>
        /// Returns the local file system path for the provided server item.
        /// </summary>
        /// <param name="serverItem">The server item to find the local path for</param>
        /// <param name="session">the version control session to search within</param>
        /// <returns>The found local path or throws MappingNotFoundException</returns>
        public static string GetLocalPathForServerItem(string serverItem, VersionControlSession session)
        {
            return GetLocalPathForServerItem(serverItem, session, true);
        }

        internal static string GetLocalPathForServerItem(string serverItem, VersionControlSession session, bool useTarget)
        {
            if (session == null)
            {
                throw new ArgumentNullException("session");
            }

            if (string.IsNullOrEmpty(serverItem))
            {
                throw new ArgumentNullException(MigrationToolkitVCResources.ServerItemIsNullOrEmpty, "serverItem");
            }

            VersionControlMapping mapping = FindAppropriateMappingForServerPath(serverItem, session);
            if (mapping != null)
            {
                string localRoot = TfsUtil.GetLocalPathForMapping(mapping, session, useTarget);

                int length = mapping.Target.Length;

                string remainBits = serverItem.Substring(length);
                remainBits = remainBits.Replace('/', '\\').TrimStart(new char[]{'\\'});

                return Uri.UnescapeDataString(Path.Combine(localRoot, remainBits));
            }

            throw new MappingNotFoundException(
                string.Format(MigrationToolkitVCResources.Culture, 
                MigrationToolkitVCResources.VCMappingMissing, 
                serverItem));
        }

        /// <summary>
        /// Finds and returns the mapping for the provided server path
        /// </summary>
        /// <param name="serverPath">The path whose mapping is needed</param>
        /// <param name="session">The version control session to search in</param>
        /// <returns>The found mapping or null if not found</returns>
        public static VersionControlMapping FindAppropriateMappingForServerPath(string serverPath, VersionControlSession session)
        {
            if (session == null)
            {
                throw new ArgumentNullException("session");
            }

            if (string.IsNullOrEmpty(serverPath))
            {
                throw new ArgumentNullException("serverPath");
            }

            foreach (VersionControlMapping m in session.Mappings)
            {
                if (!m.Cloak)
                {
                    if (VersionControlPath.IsSubItem(serverPath, m.Target))
                    {
                        return m;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Given a path, find the mapped path according to session mappings
        /// </summary>
        /// <param name="path">the path to be searched</param>
        /// <param name="session">the version control session</param>
        /// <param name="useTarget">Given path is source path if this value is set to true</param>
        /// <returns></returns>
        public static string FindMappedPath(string path, VersionControlSession session, bool useSource)
        {
            if (session == null)
            {
                throw new ArgumentNullException("session");
            }

            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

            try
            {
                foreach (VersionControlMapping m in session.Mappings)
                {
                    if (!m.Cloak)
                    {
                        if (useSource)
                        {
                            if (VersionControlPath.IsSubItem(path, m.Source))
                            {
                                return VersionControlPath.Combine(VersionControlPath.GetFullPath(m.Target),
                                    VersionControlPath.GetFullPath(path).Substring(VersionControlPath.GetFullPath(m.Source).Length).TrimStart(VersionControlPath.Separator));                        
                            }
                        }
                        else
                        {
                            if (VersionControlPath.IsSubItem(path, m.Target))
                            {
                                return VersionControlPath.Combine(VersionControlPath.GetFullPath(m.Source), 
                                    VersionControlPath.GetFullPath(path).Substring(VersionControlPath.GetFullPath(m.Target).Length).TrimStart(VersionControlPath.Separator));
                            }
                        }
                    }
                }
            }
            catch( VersionControlException e)
            {
                // This is usually a InvalidPath exception.
                Trace.TraceError(e.Message);
            }
            return null;
        }

        /// <summary>
        /// Deletes the specified file.  Removes the ReadOnly attribute if it is set on the
        /// file.
        /// </summary>
        /// <param name="localPath">The local file to delete</param>
        public static void DeleteFile(string localPath)
        {
            if (string.IsNullOrEmpty(localPath))
            {
                throw new ArgumentNullException("localPath");
            }

            if (File.Exists(localPath))
            {
                FileAttributes attr = File.GetAttributes(localPath);
                if ((attr & FileAttributes.ReadOnly) != 0)
                {
                    attr &= ~FileAttributes.ReadOnly;
                    File.SetAttributes(localPath, attr);
                }

                File.Delete(localPath);
            }
        }

        /// <summary>
        /// If the parent directory to the specified file does not exist the parent directory is created.
        /// </summary>
        /// <param name="path">The file whose parent directory should be created.</param>
        public static void EnsurePathToFileExists(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

            string directoryName = Path.GetDirectoryName(path);
            Directory.CreateDirectory(directoryName);
        }

        /// <summary>
        /// Logs the provided error and throws a MigrationException
        /// </summary>
        /// <param name="message">The message to log and throw</param>
        public static void FailWithError(string message)
        {
            if (message == null)
            {
                throw new ArgumentNullException("message");
            }

            TraceManager.TraceError(message);
            throw new MigrationException(message);
        }

        /// <summary>
        /// Logs the provided formatted error string and throws a MigrationException
        /// </summary>
        /// <param name="format">The error message format</param>
        /// <param name="args">The formatted error message arguments</param>
        public static void FailWithError(string format, params object[] args)
        {
            if (format == null)
            {
                throw new ArgumentNullException("format");
            }

            if (args == null)
            {
                throw new ArgumentNullException("args");
            }

            FailWithError(
                string.Format(MigrationToolkitResources.Culture,format, args));
        }

        /// <summary>
        /// Returns the local path for the provided version control mapping
        /// </summary>
        /// <param name="mapping">The mapping whose local path is needed</param>
        /// <param name="session">The version control session to search through.</param>
        /// <returns>The local file path for the version control mapping</returns>
        public static string GetLocalPathForMapping(VersionControlMapping mapping, VersionControlSession session)
        {
            return GetLocalPathForMapping(mapping, session, true);
        }

        /// <summary>
        /// Correct a shelveset name if it is longer than 64 character or contains invalid characters /\
        /// </summary>
        /// <param name="shelvesetName">The shelveset name to be corrected</param>
        /// <returns></returns>
        internal static string CorrectShelvesetname(string shelvesetName)
        {
            if (shelvesetName.Length >= 64)
            {
                shelvesetName = shelvesetName.Substring(0, 31) + Guid.NewGuid().ToString("N"); ;
            } 
            return shelvesetName.Replace('/', '_').Replace(':', '_').Replace('<', '_').Replace('>', '_').Replace('\\', '_').Replace('|', '_').Replace('?', '_').Replace(';', '_').TrimEnd(' ');
        }

        internal static string GetLocalPathForMapping(VersionControlMapping mapping, VersionControlSession session, bool useTarget)
        {
            if (mapping == null)
            {
                throw new ArgumentNullException("mapping");
            }

            if (session == null)
            {
                throw new ArgumentNullException("session");
            }

            // in the TFS-to-TFS case we need to slip this.
            string root = (useTarget) ? session.Target.WorkspaceRoot : ((TfsVersionControlTargetEndpoint)session.Source).WorkspaceRoot;
            string map = (useTarget) ? mapping.Target : mapping.Source;

            return Path.Combine(
                    root,
                    map.Substring(2).Replace('/', '\\'));
        }

        /// <summary>
        /// Determines if the provided path is a valid TFS server path
        /// </summary>
        /// <param name="path">The path to test</param>
        /// <param name="throwOnError">If true a MigrationException is thrown if the path is not valid.  if false, false will be returned if the path is not valid.</param>
        /// <returns>True if the path is valid or false if it is not and if throwOnError is false.</returns>
        public static bool IsValidTfsServerPath(string path, bool throwOnError)
        {
            bool isValid = false;
            string errorMsg = null;

            if (!string.IsNullOrEmpty(path))
            {
                if (path.Length <= 260)
                {
                    // server paths always start with the root node
                    if (path.StartsWith("$/"))
                    {
                        // server paths use '/' not '\'
                        if (path.IndexOf('\\') == -1)
                        {
                            // path segment starts with '$'
                            if (!path.Contains("/$"))
                            {
                                isValid = true;
                            }
                            else
                            {
                                errorMsg = string.Format(
                                    MigrationToolkitVCResources.Culture,
                                    MigrationToolkitVCResources.InvalidServerPath_DollarSegment,
                                    path);
                            }
                        }
                        else
                        {
                            errorMsg = string.Format(
                                MigrationToolkitVCResources.Culture,
                                MigrationToolkitVCResources.InvalidServerPath_WrongSlashes,
                                path);
                        }
                    }
                    else
                    {
                        errorMsg = string.Format(
                            MigrationToolkitVCResources.Culture,
                            MigrationToolkitVCResources.InvalidServerPath_MustStartWithDollarSlash,
                            path);
                    }
                }
                else
                {
                    errorMsg = string.Format(
                        MigrationToolkitVCResources.Culture,
                        MigrationToolkitVCResources.InvalidServerPath_260Limit,
                        path);
                }
            }
            else
            {
                errorMsg = MigrationToolkitVCResources.InvalidServerPath_NullOrEmpty;
            }

            if (!isValid)
            {
                Debug.Assert(!string.IsNullOrEmpty(errorMsg));

                TraceManager.TraceWarning(errorMsg);
                if (throwOnError)
                {
                    throw new MigrationException(errorMsg);
                }
            }

            return isValid;
        }

        /// <summary>
        /// Combines two paths and ensures that only one forward slash seperates the two paths. 
        /// </summary>
        /// <param name="part1">The first part of the path to combine</param>
        /// <param name="part2">The second part of the path to combine</param>
        /// <returns>The combines path with a single forward slash between the two parts.</returns>
        public static string ConcatWithoutDoubleSlashes(string part1, string part2)
        {
            if (part1 == null)
            {
                throw new ArgumentNullException("part1");
            }

            if (part2 == null)
            {
                throw new ArgumentNullException("part2");
            }

            if (part1.IndexOf('\\') != -1)
            {
                part1 = part1.Replace('\\', '/');
            }

            if (part2.IndexOf('\\') != -1)
            {
                part2 = part2.Replace('\\', '/');
            }

            if (part1.EndsWith("/"))
            {
                part1 = part1.TrimEnd(slashArray);
            }

            if (part2.StartsWith("/"))
            {
                part2 = part2.TrimStart(slashArray);
            }

            return string.Format(
                CultureInfo.InvariantCulture,
                "{0}/{1}", part1, part2);
        }

        /// <summary>
        /// Compare 'Action', 'Source', and 'Target' of the current item with the provided item. 
        /// The current item is the child of the provided item if 
        /// 1. Actions are the same 
        /// 2. Parent items's Target is the sub-item of the current item's Target. 
        /// 3. For rename/branch/merge, Parent item's Source is the sub-item of the item's Source and postfix sould be the same. 
        /// </summary>
        /// <param name="parentItem">Item to be compared</param>
        /// <returns>True if the provided item is the parent of the current item.</returns>
        public static bool isChildItemOf(BatchedItem item, BatchedItem parentItem)
        {
            if ((parentItem == null) || (parentItem.Target == null) || (item == null) || (item.Target == null) )
            {
                return false;
            }
     
            if ((item.Action == parentItem.Action) && (VersionControlPath.IsSubItem(item.Target, parentItem.Target)))
            {
                if (((item.Action == ChangeAction.Branch) || (item.Action == ChangeAction.Merge) || (item.Action == ChangeAction.Rename))
                    && item.Source != null && parentItem.Source != null && (VersionControlPath.IsSubItem(item.Source, parentItem.Source)))
                {
                    //Construct a canonlized serverpath instead of a truncated path to avoid assertion failure in TFC debug build.
                    string sourcePostFix = item.Source.Substring(parentItem.Source.Length);
                    string constructedTarget = ConcatWithoutDoubleSlashes(parentItem.Target, sourcePostFix);
                    if ( !VersionControlPath.EqualsCaseSensitive(item.Target, constructedTarget))
                    {
                        return false;
                    }
                }
                return true;
            }
            return false;
        }

        static char[] slashArray = new char[] { '/' };

    }
}
